package com.moonshineproject.plugin.actionscript.as3parser.completers
{
	import __AS3__.vec.Vector;
	
	import com.moonshineproject.plugin.actionscript.as3parser.MetadataManager;
	import com.moonshineproject.plugin.actionscript.as3parser.parsers.ImportParser;
	import com.moonshineproject.plugin.actionscript.as3parser.vo.FunctionVO;
	import com.moonshineproject.plugin.actionscript.as3parser.vo.MemberBase;
	import com.moonshineproject.text.vo.CompletionResult;
	
	import mx.collections.ArrayCollection;
	
	import org.as3commons.asblocks.parser.api.IParserNode;
	
	public class DotCompleter extends BaseCompleter
	{
		protected var node:IParserNode;
		protected var parents:Array;
		protected var metadata:MetadataManager;
		protected var importParser:ImportParser;

		protected var completeThisClass:Boolean;
		protected var ignoreBlockDefinitions:Boolean;
		
		// If we want to consider variables defined inside a function we set this
		public var blockDefinitions:Vector.<MemberBase>;
		public var filterCharStart:int = -1;
		
		
		public function DotCompleter(node:IParserNode, parents:Array, meta:MetadataManager, importParser:ImportParser)
		{
			this.node = node;
			this.parents = parents;
			this.metadata = meta;
			this.importParser = importParser;
		}

		public function getCompletion(targetNode:IParserNode, lastChar:String, caretIdx:int):ArrayCollection
		{	
			var type:String = getTypeToList();
			if (!type) return null;
			
			// If we list this class we need to list inherited stuff as well
			var accessLevel:int = (completeThisClass) ? AccessLevels.PRIVATE : AccessLevels.PUBLIC;
			
			// Now we know the type to be completed, fetch all it's members
			var members:Vector.<MemberBase> = metadata.listInstanceMembersFor(type, accessLevel);
			if (!members) return null;
			
			var completions:ArrayCollection = new ArrayCollection();
			
			for (var i:int = 0; i < members.length; i++)
			{
				var member:MemberBase = members[i];
				var cr:CompletionResult = new CompletionResult();
				cr.completion = member.name;
				if (member is FunctionVO)
				{
					if (FunctionVO(member).parameters.length > 0)
					{
						// TODO: Offset caret by -1 so it's inside () when completion is done..?
						cr.completion += "()";	
					}
					else
					{
						cr.completion += "()";
					}
				}
				
				cr.label = member.toString();
				completions.addItem(cr);
			}

			// Figure out context
			if (targetNode.kind == "primary")
			{
				// Dot means that targetNode is the node before that dot
				if (lastChar == ".")
				{
					filterCharStart = caretIdx;
					//filterCharStart = targetNode.column + targetNode.stringValue.length;
				}
				else
				{
					filterCharStart = targetNode.column-1;	
				}
			}
			else if (targetNode.kind == "this")
			{
				filterCharStart = targetNode.column+"this.".length-1;
			}
			else if(targetNode.kind == "arguments")
			{
				filterCharStart = caretIdx;
			} 
			
			applySort(completions);			

			return completions;
		}

		protected function getTypeToList():String
		{
			// Look back and find all nodes involved
			var nodeChain:Array = resolveNodeChain(node, parents.concat());
			// Resolve from left-to-right
			var clazz:String = resolveDotTypeChain(nodeChain);
			
			return clazz;
		}
		
		protected function resolveNodeChain(node:IParserNode, parents:Array):Array
		{
			var typesToResolve:Array = [];
			for (var i:int = parents.length-1; i > 0; i--)
			{
				var node:IParserNode = parents[i];
				
				if (node.kind == "dot" && node.parent.kind != "call" && node.parent.kind != "arguments")
				{
					if (node.children[0].kind == "call")
					{
						typesToResolve.unshift( node.children[0].children[0].stringValue );
					}
					else if (node.children[0].stringValue)
					{
						typesToResolve.unshift( node.children[0].stringValue );						
					}
					
				}
				else if (node.kind == "this" && node.parent.kind != "call" && node.parent.kind != "arguments")
				{
					ignoreBlockDefinitions = true;
					// We don't need the 'this' node
					return typesToResolve;
				}
				else if (node.kind == "arguments" && node.parent.kind != "call")
				{
					typesToResolve.unshift( node.children[0].stringValue );
				}
				
				/*
				TODO: return on 'call' and possibly others
				example: foo.bar.someFunction(this.|); // don't resolve full chain, just 'this'
				else if (node.kind == "call")
				{
					return;
				}
				*/
			}
			
			return typesToResolve;
		}
		
		// Take ['this', 'width', ...]
		protected function resolveDotTypeChain(chain:Array):String
		{
			if (chain.length == 0) return lookupType("this", null);
			
			// We always assume 'this' since "foo" and "this.foo" should yield the same result.
			// As a side effect we get 'this' if the chain is empty (both those case should just list this class)
			var type:String = "this";
			var property:String;
			for (var i:int = 0; i < chain.length; i++)
			{
				property = chain[i];
				type = lookupType(type, property);
			}
			
			return type;
		}
		
		// Return type as String
		//  example: package.NodeFinder
		protected function lookupType(className:String, property:String):String
		{
			var orgClassName:String = className;
			if (className == "this") 
			{
				// Dive down in the AST & locate the fully qualified class name
				var packageName:String = "";
				for (var i:int = 0; i < parents.length; i++)
				{
					var node:IParserNode = parents[i];
					if (node.kind == "package") 
					{
						for (var m:int = 0; m < node.children.length; m++)
						{
							var child:IParserNode = node.children[m];
							if (child.kind == "name")
							{
								packageName = child.stringValue + ".";
								break;
							}
						}
					}
					else if (node.kind == "class")
					{
						for (m = 0; m < node.children.length; m++)
						{
							child = node.children[m];
							if (child.kind == "name")
							{
								className = packageName + child.stringValue;
								break;
							}
						}
					}	
				}
				// If no classname was found we can't do anything
				if (!className)
				{
					return null;
				}
				// If no property is given we list this class
				if (!property)
				{
					// Note this so we can list inherited props
					completeThisClass = true;
					return className;
				}
			}
			
			if (!className) return null;
			
			var type:String;
			if (!ignoreBlockDefinitions && blockDefinitions) 
			{
				// Find the type of a variable (function scope + class scope);
				type = findBlockDefinitionOf(property);
				// If not found we turn to class scope
				if (!type) type = metadata.getTypeOf(className, property);
			}
			else 
			{
				// Find the type of a variable (class scope)
				type = metadata.getTypeOf(className, property);	
			}
			
			
			// Constructors return their own class (also usable for casts)
			if (property == className)
			{
				return className;
			}
			else if (type == null && orgClassName == "this")
			{
				// Could also be a cast: 'Foo(this.bar).'
				var pkg:String = importParser.getPackageNameForType(property);
				if (pkg)
				{
					property = pkg + "." + property; 
				}
				
				return property;
			}
			trace("DotCompleter.lookupType: resolved " +className + "." + property + " to " + type);
			
			return type;
		}
		
		protected function findBlockDefinitionOf(property:String):String
		{	
			for each (var member:MemberBase in blockDefinitions)
			{
				if (member.name == property)
				{
					return member.getFullyQualifiedName();
				}
			}
			
			return null;
		}

	}
}